﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Xml.Linq;

namespace Duellum.Core
{
	/// <summary>
	/// base class for objects that can have Augmented Properties
	/// </summary>
	public abstract class AugObject //TODO: implement this interface System.ComponentModel.INotifyPropertyChanged
	{
		public abstract AugObject AugParent { get; }
		
		//private object propValuesSyncRoot = new object();
		private IDictionary<AugProperty,IAugValue> AugValues; //dictionary of values is lazily created
		
		public IEnumerable<KeyValuePair<AugProperty,IAugValue>> GetLocalValues()
		{
			var propValues = this.AugValues; //avoid race condition
			return (
					(propValues != null)
					? propValues.ToArray()
					: Enumerable.Empty<KeyValuePair<AugProperty,IAugValue>>()
				);
		}

		public T GetValue<T>(AugProperty ap)
		{
			return (T)GetValue(ap);
		}
		
		public object GetValue(AugProperty ap)
		{
			return GetValue(ap, true);
		}
		
		private object GetValue(AugProperty ap, bool autoSetDefaultValue)
		{
			IAugValue augValue;
			if (TryGetLocalAugValue(ap, out augValue)) {
				return augValue.GetValue(this);
			} else {
				var value = GetBaseValue(ap);

				if (value == AugProperty.UnsetValue) {
					var behav = ap.GetBehavior(this.GetType());

					var defaultValue = behav.GetDefaultValue();
					value = defaultValue;
					
					var facDefaultValue = defaultValue as IAugValueFactory;
					if (facDefaultValue != null) value = facDefaultValue.GetValue(this);

					if (autoSetDefaultValue && defaultValue is IAugValueFactory) this.SetValue(ap, value);

					var valueAsAug = value as IAugValue;
					if (valueAsAug != null) value = valueAsAug.GetValue(this);

					value = ap.ConvertToPropertyType(value);
				}

				return value;
			}
		}

		public object GetBaseValue(AugProperty ap)
		{
			if (ap.Inherits && AugParent != null) {
				return AugParent.GetInheritedValue(ap, this);
			} else {
				return AugProperty.UnsetValue;
			}
		}

		public void SetValue(AugProperty ap, IAugValue value)
		{
			SetLocalAugValue(ap, value);
		}
		
		public void SetValue(AugProperty ap, object value)
		{
			//TODO: [or not] validate value (using validate callback, if any) (might be useful for enum properties)
			
			//TODO: [or not] Coerce value (using coerce callback, if any) (what about a coerce event?)
			// Coercing should keep original value. It works like a FunctionAugValue
			
			value = ap.ConvertToPropertyType(value);
			
			SetLocalAugValue(ap, ConstantAugValue.Create(value));
		}
		
		public void CombineValue(AugProperty ap, Delegate func)
		{
			SetLocalAugValue(ap, FunctionAugValue.Create(func, ap));
		}
		
		public void CombineValue(AugProperty ap, LambdaExpression expr)
		{
			SetLocalAugValue(ap, FunctionAugValue.Create(expr, ap));
		}

		public void ClearValue(AugProperty ap)
		{
			InternalClearAugValue(ap);
		}
		
		protected virtual IDictionary<AugProperty,IAugValue> CreateAugValueDictionary()
		{
			return new Dictionary<AugProperty,IAugValue>();
		}
		
		private bool TryGetLocalAugValue(AugProperty ap, out IAugValue value)
		{
			value = null;
			if (AugValues == null) return false;
			
			return AugValues.TryGetValue(ap, out value);
		}

		private void SetLocalAugValue(AugProperty ap, IAugValue value)
		{
			if (AugValues == null) AugValues = CreateAugValueDictionary();
			
			object oldValue = this.GetValue(ap, false);
			
			if (IsDefaultValue(ap, value)) {
				//Remove augvalue because it's the same as default
				AugValues.Remove(ap);
			} else {
				//Set (overwriting) the augvalue for the property
				AugValues[ap] = value;
			}
			
			AugValueChangedEventArgs valueChangedEvArgs = new AugValueChangedEventArgs(ap, oldValue, value.GetValue(this));
			
			var behav = ap.GetBehavior(this.GetType());
			behav.OnValueChanged(this, valueChangedEvArgs);
		}
		
		private bool IsDefaultValue(AugProperty ap, IAugValue value)
		{
			if (!(value is ConstantAugValue)) return false;
			
			var behav = ap.GetBehavior(this.GetType());
			
			return ((ConstantAugValue)value).Value == behav.GetDefaultValue();
		}

		private void InternalClearAugValue(AugProperty ap)
		{
			if (AugValues == null) return;
			
			AugValues.Remove(ap);
		}

		private object GetInheritedValue(AugProperty ap, AugObject orignalOwner)
		{
			IAugValue value;
			if (TryGetLocalAugValue(ap, out value)) {
				return value.GetValue(orignalOwner);
			} else {
				return GetBaseValue(ap);
			}
		}

		#region XML Representation
		
		public virtual XElement ToXml()
		{
		    return this.ToXml(-1);
		}

		public virtual XElement ToXml(int maxSubLevels)
		{
			return ToXml(maxSubLevels, null);
		}

		public virtual string GetTypeName()
		{
			return this.GetType().Name;
		}

		protected virtual XElement ToXml(int maxSubLevels, params XObject[] content)
		{
			var xElem = new XElement(GetTypeName());
			
			if (content != null) xElem.Add(content);
			
			xElem.Add(this.GetLocalValues().Select(pair => pair.ToXml()));
			
			if (maxSubLevels != 0 && this.AugParent != null) xElem.Add(this.AugParent.ToXml(maxSubLevels - 1));
			
			return xElem;
		}
		
		#endregion
	}
}
